用VBoxDbg调试并理解单线程版脏牛(CVE-2016-5195)
1、序言
脏牛是前几年比较流行的一个漏洞,网上关于该漏洞的分析也比较多。但是对于像UP主一样的新手而言,始终未能看到漏洞触发的第一现场,比如像OOB/UAF等漏洞可以直接通过调试器观察内存看到漏洞确实可以触发成功。但脏牛却不是这样,主要因为双机调试Linux的时候,KGDB无法对页表(物理内存)进行读写。再加上多线程Race也提升了调试的难度,所以一直没能感觉很好的看到脏牛触发的第一现场到底发生了什么,直到我遇到了VBoxDbg这个调试器。
2、VboxDbg说明
在Windows平台下,使用Windbg可以对物理内存和一些特殊寄存器进行读写。但是在Linux内核调试的时候,就不那么方便,因为(严格来说)KGDB+Linux内核没有实现这样的功能。即便可以通过编写proc伪文件系统模块来对物理内存进行读写,但还是没有Windbg那种方式感觉方便。于是UP主找了各种资料,最后终于发现一款比较好用的调试器VboxDbg。
VBoxDbg是VirtualBox内建的一款调试器,主要用于调试在VirtualBox内运行的Guest主机。VBoxDbg也可以查看Guest机器的物理内存和GDTR/IDTR等信息。
3、单线程版DirtyCow
脏牛能触发的一个必要原因是:双线程竞争执行、对第四级页表中的PTE进行读写操作。
为了更好的凸显脏牛的Race的问题,我把网上的公开POC修改为一个单线程版本的脏牛poc,具体如下:
void *map;
int f;
struct stat st;
char *name;
void *worker_write(void *arg)
{
char *str;
str=(char*)arg;
int f=open("/proc/self/mem",O_RDWR);
int i,c=0;
lseek(f,(uintptr_t) map,SEEK_SET);
write(f,str,strlen(str));
printf("procselfmem %d\n\n", c);
}
int main(int argc,char *argv[])
{
if (argc<3)
{
(void)fprintf(stderr, "%s\n","usage: dirtycowtest target_file new_content");
return 1;
}
f=open(argv[1],O_RDONLY); //这里由于打开的是root权限文件,所以理论上来说使用普通用户权限是无法对root文件进行修改的
fstat(f,&st);
name=argv[1];
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
printf("mmap %zx\n\n",(uintptr_t) map);
worker_write(argv[2]); //这里尝试进行了写操作
return 0;
}
可以看到,代码中只有一个线程,且该线程仅仅执行了一个write动作。我们等下会用VBoxDbg来模拟第二个线程原本的动作(madvise)
4、调试单线程版DirtyCow
调试的总体思路为:
使用GDB+KGDB在follow_page_mask函数调用返回后的第一条内核指令、以及faultin_page函数内部调用handle_mm_fault函数返回后的第一条内核指令处分别下内存访问断点;
使用GDB+KGDB监控用户态调用mmap后返回地址处的内存信息;
在第二次调用handle_mm_fault后借助VBoxDbg手动修改PTE信息并让程序运行起来,观察结果。
由于Dirty Cow与ASLR/KASLR无关,所以在本实验中为了方便调试已经手动关闭了ASLR
首先直接运行修改过的dirtycowtest(运行结果如图1),可以发现,运行后sensitive.txt文件信息并未发生改变,同时记录下mmap返回地址:0x7ffff7ff7000
图1 直接运行dirtycowtest发现root权限文件未被修改
然后再次运行dirtycowtest程序,同时使用GDB+KGDB观察在__get_user_pages函数内部的执行情况,情况如下(如图2):
图2 第一次调用follow_page_mask后断下
可以看到,当前处于刚刚执行完follow_page_mask的时刻(图2),并且此时由于页面未被调入物理内存中,所以用户态地址0x7ffff7ff7000处于无法访问的状态。且此时执行follow_page_mask的返回结果也为0(表示返回错误,未获取到struct
page信息)。接下来继续执行,进入到faultin_page函数内部,在handle_mm_fault函数执行完毕后断下:
图3 第一次调用handle_mm_fault后断下
此时(图3)可以看到,地址0x7ffff7ff7000可以被访问到了(页表中PTE状态为present),并且结果为0x000a363534333231(sensitive.txt的文件内容的二进制形式表示)。然后继续执行:
图4 第二次调用follow_page_mask后断下
当再次执行完follow_page_mask函数时(图4),发现虽然地址0x7ffff7ff7000可以被访问了,但follow_page_mask返回值page依然为0。这是由于foll_flags与页表PTE记录的权限信息不符,因此触发了一个写异常。继续执行:
图5 第二次调用handle_mm_fault后断下
此时(图5)是第二次执行完handle_mm_fault函数,也就是此时内核完成了COW页面的生成过程(但由于是同一进程访问自身内存,所以实际上只有一个物理页面)。然后就需要使用VBoxDbg进行后续工作了:
首先通过已知的用户态地址0x7ffff7ff7000,可以计算出在x86_64环境中,其四级页表的每级偏移分别为:0x7F8、0xFF8、0xDF8、0xFB8。这里我们要使用到VBoxDbg的四个命令,分别为:
命令 | 描述 |
stop | 让VirtualBox彻底暂停,此时再操作GDB+KGDB并不会看到回显 |
dpta | 可以解析指定物理PTE信息 |
eq | 以8字节为单位长度,修改指定地址内容 |
g | 让虚拟机继续运行,相当于stop命令的反操作 |
结合cr3寄存器信息,和上述已知四个页表的偏移信息,可以计算出位于第四级页表中的PTE信息。然后手动对该PTE清零,即可模拟原本Dirty Cow漏洞exploit中的第二个madvise线程工作,具体操作记录如图6:
图6 使用VBoxDbg修改PTE
随后禁用GDB+KGDB环境中的所有断点继续执行,即可从虚拟机中看到执行结果。对比图1中的结果可以发现,虽然dirtycowtest程序没有发生变化,但是确实完成了对sensitive.txt文件的修改(已修改为111111),结果如图7:
图7 利用dirtycowtest成功使用低权限用户修改root权限文件内容
至此,完成了DirtyCow单线程版本的调试。在进行页表或物理内存调试等工作时,VBoxDbg有着GDB/KGDB无法比拟的优势。VBoxDbg还有很多功能,本文无法一一介绍。总体来说,VBoxDbg是一款可以与GDB/KGDB互补的简单好用的调试工具。
5、参考文档
Dirty Cow(CVE-2016-5195)
VirtualBox User Manual
看雪ID:OxLucifer
bbs.pediy.com/user-733628
本文由看雪论坛 OxLucifer 原创
转载请注明来自看雪社区